/**
 * This file is part of the CAKIS project.
 * 
 * Copyright (c) 2009-2010 - Florian Arndt, Christian Kerl
 * 
 * @license GNU Lesser General Public License v3 <http://www.opensource.org/licenses/lgpl-3.0.html>
 * @since   11.05.2010
 * @version SVN: $Id: ChefkochRecipeImportService.java 135 2010-05-15 10:35:11Z christian-kerl@web.de $
 */
package org.cakis.server.service.impl;

import java.net.MalformedURLException;
import java.net.URL;

import org.cakis.server.Names;
import org.cakis.server.model.ProductQuantity;
import org.cakis.server.model.ProductType;
import org.cakis.server.model.Recipe;
import org.cakis.server.model.RecipeIngredient;
import org.cakis.server.model.StockUnit;
import org.cakis.server.service.RecipeImportService;
import org.cakis.server.service.util.HtmlConstants;
import org.cakis.server.service.util.HtmlContentHandlerExecutor;
import org.cakis.server.service.util.NullLog;
import org.cakis.server.service.util.URLStreamFactory;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.Startup;
import org.jboss.seam.log.Log;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Implementation of the {@link RecipeImportService} interface for http://www.chefkoch.de
 * providing the following information:
 * - ...
 * 
 * @author Christian Kerl
 */
@Name(Names.ChefkochRecipeImportService)
@Scope(ScopeType.APPLICATION)
@Startup
public class ChefkochRecipeImportService extends RecipeImportServiceRegistryImpl.SelfRegisteringRecipeImportService implements RecipeImportService {

    private static final String Name = "chefkoch.de";
    private static final String BaseUrl = "http://www.chefkoch.de";
    private static final String Charset = "ISO-8859-1";

    @Logger
    private Log log = NullLog.DEFAULT;

    private URLStreamFactory urlStreamFactory;

    @Override
    public String getName() {
        return Name;
    }

    @Override
    public URL getUrl() {
        try {
            return new URL(BaseUrl);
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }

    public ChefkochRecipeImportService() {
        this(URLStreamFactory.DEFAULT);
    }

    public ChefkochRecipeImportService(URLStreamFactory factory) {
        this.urlStreamFactory = factory;
    }

    @Override
    public Recipe getRecipeInformation(URL url) {
        if(url == null || !getUrl().getHost().equals(url.getHost())) {
            throw new IllegalArgumentException("url");
        }

        RecipePageContentHandler handler = new RecipePageContentHandler(log);

        log.info("Parsing Recipe #0", url);

        HtmlContentHandlerExecutor<RecipePageContentHandler> executor = new HtmlContentHandlerExecutor<RecipePageContentHandler>(urlStreamFactory, handler, Charset);
        Recipe result = executor.execute(url).getResult();

        if(result != null) {
            log.info("Parsed Recipe #0", result.getName());
        } else {
            log.warn("Parsing Recipe #0 failed!", url);
        }

        return result;
    }

    private static class RecipePageContentHandler extends DefaultHandler implements HtmlConstants {
        private static enum State {
            ExpectContent,
            ExpectNameHeading,
            ExpectName,
            ExpectIngredientPanel,
            ExpectNumberOfPersons,
            ExpectIngredientTable,
            ExpectIngredient,
            ExpectIngredientQuantity,
            ExpectIngredientName,
            ExpectPreparationPanel,
            ExpectPreparationText,
            Done
        }

        // state transitions:
        // ExpectContent            -> ExpectNameHeading
        // ExpectNameHeading        -> ExpectName
        // ExpectName               -> ExpectIngredientPanel
        // ExpectIngredientPanel    -> ExpectNumberOfPersons
        // ExpectNumberOfPersons    -> ExpectIngredientTable
        // ExpectIngredientTable    -> ExpectIngredient
        // ExpectIngredient         -> ExpectIngredientQuantity | ExpectPreparationPanel
        // ExpectIngredientQuantity -> ExpectIngredientName
        // ExpectIngredientName     -> ExpectIngredient
        // ExpectPreparationPanel   -> ExpectPreparationText
        // ExpectPreparationText    -> Done

        private static final String ContentId = "content";
        private static final String IngredientsId = "rezept-zutaten";
        private static final String PreparationId = "rezept-zubereitung";
        private static final String NumberOfPersonsName = "divisor";
        private static final String IngredientsClass = "zutaten";
        private static final String NameClass = "big";


        private final Log log;
        private final Recipe result;
        private State state;
        private String currentIngredientQuantity;
        private String currentIngredientName;
        private RecipeIngredient currentIngredient;
        private String currentPreparation;

        public Recipe getResult() {
            return state == State.Done ? result : null;
        }

        public RecipePageContentHandler(Log log) {
            this.log = log;
            this.result = new Recipe();
            this.state = State.ExpectContent;
        }

        @Override
        public void startElement(String uri, String name, String qName, Attributes attr)
        throws SAXException {
            switch(state) {
            case ExpectContent:
                if(DivElement.equals(name) && ContentId.equals(getAttrValue(attr, IdAttribute))) {
                    state = State.ExpectNameHeading;
                }
                break;
            case ExpectNameHeading:
                if(H1Element.equals(name) && NameClass.equals(getAttrValue(attr, ClassAttribute))) {
                    state = State.ExpectName;
                }
                break;
            case ExpectIngredientPanel:
                if(DivElement.equals(name) && IngredientsId.equals(getAttrValue(attr, IdAttribute))) {
                    state = State.ExpectNumberOfPersons;
                }
                break;
            case ExpectNumberOfPersons:
                if(InputElement.equals(name) && NumberOfPersonsName.equals(getAttrValue(attr, NameAttribute))) {
                    handleNumberOfPersons(getAttrValue(attr, ValueAttribute));
                    state = State.ExpectIngredientTable;
                }
                break;
            case ExpectIngredientTable:
                if(TableElement.equals(name) && IngredientsClass.equals(getAttrValue(attr, ClassAttribute))) {
                    state = State.ExpectIngredient;
                }
                break;
            case ExpectIngredient:
                if(TrElement.equals(name)) {
                    currentIngredientQuantity = "";
                    currentIngredientName = "";
                    currentIngredient = new RecipeIngredient();
                    currentIngredient.setRecipe(result);
                    state = State.ExpectIngredientQuantity;
                }
                break;
            case ExpectPreparationPanel:
                if(DivElement.equals(name) && PreparationId.equals(getAttrValue(attr, IdAttribute))) {
                    currentPreparation = "";
                    state = State.ExpectPreparationText;
                }
                break;
            case ExpectPreparationText:
                if(BrElement.equals(name)) {
                    currentPreparation += System.getProperty("line.separator");
                }
            }

        }

        @Override
        public void endElement(String uri, String name, String qName) throws SAXException {
            switch(state) {
            case ExpectIngredient:
                if(TableElement.equals(name)) {
                    state = State.ExpectPreparationPanel;
                }
                break;
            case ExpectIngredientQuantity:
                if(TdElement.equals(name)) {
                    handleIngredientQuantity(currentIngredient, currentIngredientQuantity.trim());
                    state = State.ExpectIngredientName;
                }
                break;
            case ExpectIngredientName:
                if(TdElement.equals(name)) {
                    handleIngredientName(currentIngredient, currentIngredientName);
                    result.getIngredients().add(currentIngredient);
                    state = State.ExpectIngredient;
                }
                break;
            case ExpectPreparationText:
                if(DivElement.equals(name)) {
                    handlePreparation(currentPreparation);
                    state = State.Done;
                }
                break;
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            String data = normalizeNbspSpaces(new String(ch, start, length)).trim();

            switch(state) {
            case ExpectName:
                handleName(data);
                state = State.ExpectIngredientPanel;
                break;
            case ExpectIngredientQuantity:
                currentIngredientQuantity += data + " ";
                break;
            case ExpectIngredientName:
                currentIngredientName += data + " ";
                break;
            case ExpectPreparationText:
                currentPreparation += data + " ";
                break;
            }
        }

        private void handleName(String data) {
            result.setName(data);
        }

        private void handleNumberOfPersons(String data) {
            Integer number = 0;

            try {
                number = Integer.parseInt(data);
            } catch(NumberFormatException e) {
                log.warn("Invalid Number #0!", data);
            }

            result.setNumberOfPersons(number);
        }

        private void handleIngredientQuantity(RecipeIngredient ingredient, String data) {
            ProductQuantity quantity = new ProductQuantity();
            quantity.setUnit(new StockUnit());

            Float pieces = 0.0f;
            String name = "";

            data = data.trim();

            if(!data.isEmpty()) {
                String[] parts = data.split(" ", 2);

                try {
                    pieces = parseNumber(parts[0]);
                } catch(NumberFormatException e) {
                }

                if(parts.length > 1) {
                    name = parseUnitName(parts[1]);
                }
            }

            quantity.getUnit().setName(name);
            quantity.setNumberOfPieces(pieces);

            ingredient.setQuantity(quantity);
        }

        private void handleIngredientName(RecipeIngredient ingredient, String data) {
            ProductType product = new ProductType();

            data = data.trim();

            String name = "";

            if(!data.isEmpty()) {
                String[] parts = data.split(" |,", 2);

                name = parts[0];

                if(name.contains("(")) {
                    String oldName = name;
                    name = oldName.substring(0, oldName.lastIndexOf("("));

                    log.info("RecipeIngredient Name #1 (stripped plural from #0)", oldName, name);
                } else {
                    log.info("RecipeIngredient Name #0", name);
                }

                if(parts.length > 1) {
                    String hint = parts[1];
                    ingredient.setHint(hint);

                    log.info("RecipeIngredient Hint #0", hint);
                }
            }

            product.setName(name);

            ingredient.setProduct(product);
        }

        private void handlePreparation(String data) {
            result.setPreparationDescription(data.trim());
        }

        private Float parseNumber(String number) throws NumberFormatException {
            String oneQuarter   = ((char) 188) + ""; // 1/4
            String twoQuarter   = ((char) 189) + ""; // 1/2
            String threeQuarter = ((char) 190) + ""; // 3/4

            number = number.replace(",", ".");
            number = number.replace(oneQuarter, "0.25");
            number = number.replace(twoQuarter, "0.5");
            number = number.replace(threeQuarter, "0.75");

            try {
                return Float.parseFloat(number);
            } catch(NumberFormatException e) {
                log.warn("Invalid Number #0!", number);
                throw e;
            }
        }

        private String parseUnitName(String str) {
            String result = str;

            if(str.endsWith("/n")) {
                result = str.substring(0 , str.length() - 2);
                log.info("RecipeIngredient Unit #1 (stripped plural #0)", str, result);
            } else {
                log.info("RecipeIngredient Unit #0", result);
            }

            return result;
        }

        private String normalizeNbspSpaces(String str) {
            return str.replace("\u00A0", " ");
        }

        private String getAttrValue(Attributes attr, String name) {
            String value = attr.getValue(name);

            return value != null ? value : "";
        }
    }
}
